Skip to content

feat: Improved runtime compatibility - #42

Merged
Pijukatel merged 29 commits into
masterfrom
claude/quirky-carson-9147px
Sep 10, 2026
Merged

feat: Improved runtime compatibility#42
Pijukatel merged 29 commits into
masterfrom
claude/quirky-carson-9147px

Conversation

@Pijukatel

Copy link
Copy Markdown
Collaborator

Make it compatible with rootful or rootless Podman or Docker (4 supported variants)

claude and others added 29 commits September 9, 2026 09:32
The runtime already speaks only the Docker Engine API, which Podman's
`podman system service` serves too, so the whole dev loop (push/build/call
for the Node and Python samples, debug mode, migrations, build abort) works
unchanged by mounting Podman's socket at /var/run/docker.sock. Two places
depended on Docker-specific daemon behaviour and are made engine-neutral:

- Dev-folder probe: Podman's compat API auto-creates a missing bind-mount
  source instead of rejecting it, so the create-only probe reported a typo'd
  path as present and left a root-owned empty directory on the host. The
  probe now binds the host's `/` read-only and stats the candidate through
  the archive-stat endpoint (HEAD /containers/{id}/archive), component by
  component with symlink following - identical results on Docker and
  Podman, no side effects. The same check now also runs at every dev-mount
  run start, since Podman would otherwise silently recreate a deleted dev
  folder as an empty directory.
- Run telemetry: Podman does not report `system_cpu_usage` on Docker's
  scale, so `docker stats`' CPU formula over-reported (~66% for a run
  throttled to 0.25 core). CPU percent is now CPU time over wall time,
  which agrees with the Docker formula on Docker.

Also: prefix-match the runtime's own container id when checking network
membership (no spurious "already connected" warning on restart), a clearer
unreachable-socket message naming Podman and DOCKER_HOST, e2e helpers that
accept CONTAINER_CLI=podman and a unix:// DOCKER_HOST for the socket mount,
and Podman setup/caveats in README, CLAUDE.MD and the requirements.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
Rootless Podman runs the runtime container under slirp4netns/pasta, where
attaching to a second network is unsupported, so the apify-api alias never
resolved and Actor containers could not reach the API. When the self-attach
fails (or the runtime is not in a container at all), every run container now
gets an `apify-api -> host-gateway` extra host, landing on the runtime's
published port 3333; the startup warning names the cause and the
`--network apify-local` alternative. Documents rootless Podman and rootless
Docker, including that paths must be readable by the rootless user.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
…e support

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
Resolves the conflicts with the browser-view feature and carries the Podman work over to the
parallel e2e layout: `pullImage` honours CONTAINER_CLI, the Playwright base images are fully qualified,
and CI's per-file e2e jobs gain an `engine` dimension that runs every file against rootless Podman as
well as Docker. Under rootless Podman browser view needs the runtime started with
`--network apify-local` (the console reaches the VNC sidecar over that network); the startup warning
and README say so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
…n apify-local

Under rootless Podman the runtime container cannot join the network, so the console could not reach
the VNC sidecar by its address there and the viewer never got the RFB greeting. The sidecar now shares
the runtime container's own network namespace in that case, on a port allocated in that namespace, and
the console connects to it on 127.0.0.1. The alias route is unchanged whenever it is available.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
A client that polls the build to SUCCEEDED and immediately starts a run against the tag (apify-client's
`build(..., { waitForFinish })` followed by `start()`) could find the tag still missing, because the
status write landed before the tag write; CI hit that window in debug-mode.test.ts. The tag now lands
first and is put back to its previous value if a racing abort refuses the SUCCEEDED write, so an aborted
build still never ends up tagged. Regression test included.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
Podman does not create a missing host directory for a bind mount (Docker does), so `podman run` with
`-v "$(pwd)/data:/data"` fails on a fresh checkout. Every documented start command now runs `mkdir -p
data` first.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
A short image name in an Actor Dockerfile (`FROM apify/actor-node:20`)
means Docker Hub on the platform and under Docker, but Podman leaves the
resolution to the host's registries.conf, which on a stock Debian/Ubuntu
install names no unqualified-search registry. `apify push` then fails
with "short-name did not resolve to an alias".

The build service now rewrites such FROM references to their
`docker.io/...` form before handing the source files to the driver, and
logs each substitution in the build log. References that already name a
registry, `scratch`, build-stage names, and variables are left alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
Podman 5 treats `size=` in a local volume's `o` option as a project-quota
request and refuses to create the volume on a filesystem without quota
support ("Volume options size and inodes not supported"), which made every
browser-view run fail at start. The tmpfs volume only ever holds the Xvfb
Unix socket, so the 8 MB cap is dropped; mode 1777 stays.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
…en the alias is missing

The documented Podman command and the CLI now pre-create the apify-local
network and start the runtime container on it (--network apify-local),
which is the direct route between Actors and the runtime on every engine,
rootless included. A container started that way has no apify-api alias, so
the driver now reads its own address on the network and gives Actor
containers a hosts entry for it; the DNS alias stays the route when the
runtime joined the network itself, and host-gateway remains the fallback
when it is not on the network at all.

A run the engine refuses to start now writes the reason into the run's log
as well as its status message, so `apify call` shows it instead of an empty
log. Docs name Podman 4 or newer and the Ubuntu 22.04 CNI config fix.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
…art containers (Podman 3.4)

Ubuntu 22.04's Podman 3.4 writes CNI network configs its own plugins
reject, so no container can start on the runtime's apify-local network
and every run failed with "CNI network apify-local not found". The driver
now retries a container that fails to start there once on the engine's
default network; if that works, the network is written off for the rest
of the process (one warning naming the cause) and later runs and
browser-view sidecars go straight to the default network.

Off the network, Actors reach the API through the host at the address the
engine itself gives every container (host.containers.internal on Podman,
host.docker.internal on Docker Desktop), read from the runtime's own hosts
file; host-gateway stays the route only when the file has no such entry
(Docker Engine), since Podman before 4.1 rejects the keyword.

The documented Podman command no longer needs a pre-created network or
--network; docs state Podman 3.4 or newer. Verified against Podman 3.4.4.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
…ork view

Actors that had to fall back to the engine's default network reached the
API through the engine's host entry, which is wrong in two cases seen on
Podman 3.4: under rootless slirp4netns that entry is the slirp gateway,
which only reaches the host's loopback when the container is started with
allow_host_loopback; on a rootful bridge it is the bridge gateway, where the
published port need not answer. The driver now reads its own hosts entry,
default gateway and interface address and picks: slirp4netns mode with the
gateway (rootless 3.x), its own bridge address (rootful bridge), or the
engine's host entry / host-gateway otherwise. Podman 3.4 rejects the
host-gateway keyword, so it is used only when the hosts file offers nothing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
…art failures

Rootless Podman on cgroups v2 refuses to start a container whose limit
needs a controller systemd did not delegate to the user; Ubuntu 22.04
delegates memory and pids but not cpu, so once the network fallback got a
run past the network error it died on the CPU quota instead. The driver
now asks Podman's own info endpoint which controllers it has, leaves out
the limits it cannot apply (run and browser-view sidecar alike), and warns
once at startup. Docker is unaffected.

A run that can start neither on apify-local nor on the default network now
reports both failures instead of only the first, which had hidden the real
reason.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
Podman 3.x is the CNI generation: Ubuntu 22.04's stock config cannot start
a container on any user-defined network, rootless cannot attach a running
container, and a failed attach on rootful Podman 3.4 was seen to tear down
the runtime container's own loopback. The driver now detects Podman 3.x
from the API's version components, skips apify-local entirely there, and
puts Actors on the default network with the route selection introduced
earlier (slirp4netns host loopback rootless, the runtime's own bridge
address rootful). Podman 4+ and Docker are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
…by name

The browser-view sidecar image and the dev-folder probe image were imported
or built under a short name and later looked up by that name. Docker and
Podman 4 resolve a short name to the local image; Podman 3.x resolves it
only through its search registries and reports the locally stored
localhost/... image as "image not known", so every browser-view run failed
to start there. Actor images are unaffected: they are referenced by id.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
…3.4 e2e leg

Podman 3.x ignores the separate `tag` parameter of the image import
endpoint and stores the browser-view sidecar as `:latest`, so the runtime
never found the image it had just imported. The tag now travels inside the
repository name (`name:tag`), which Docker, Podman 4 and Podman 3.4 all
honour.

CI gains an e2e leg on the ubuntu-22.04 runner, whose preinstalled Podman
is 3.4.4 - every e2e file, browser view included, now also runs against
rootless Podman 3.x. The runtime's own Dockerfile names its base images
fully qualified so Podman 3.4 builds it without search registries.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
…s an anonymous one)

Podman 3.x's compat API refuses a volume mount without a source ("must set
source volume"), so every dev-folder run failed to start there. The
node_modules volume that preserves the image's dependencies underneath the
bind mount is now a named per-run volume the driver removes with the run
and sweeps by name prefix after a restart; the engine still populates it
from the image on first mount.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
Rootless Podman 3.x cannot mount a tmpfs-backed volume ("cannot mount
volumes without root privileges"), so the browser-view sidecar never
started there. The X11 socket volume is now a plain local volume; the
sidecar image carries the socket directory as mode 1777 and its script
chmods it as root before waiting for the Actor's display, so the Actor's
unprivileged Xvfb can still create the socket.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
… the run

Apify's Playwright base images start through ./xvfb-entrypoint.sh inside
the working directory. A dev-folder run bind-mounts the local folder over
that directory, so unless the folder happens to carry the same file the
engine refuses to start the container ("executable file not found"). The
driver now inspects the image's command: a working-directory-relative
entrypoint (or Cmd) the dev folder does not provide is read out of the
image and placed in the container before start, and the run starts
through that copy. Absolute and PATH-resolved commands are untouched, as
is anything the dev folder provides itself.

Covered by unit tests and a new e2e case with a busybox Actor whose
entrypoint lives in its working directory.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
…tream

The new dev-folder case asserted on `apify call`'s streamed output; for an
Actor that exits within milliseconds the CLI's log stream can close before
the last lines are flushed, on every engine. The stored run log is what
the assertions are about.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
…ved entrypoint

Docker and Podman drop the image's Cmd from a create request that overrides
Entrypoint, so the preserved Xvfb wrapper ran with no program to wrap
("xvfb-run: usage error: need a command to run"). The image's Cmd now
travels with the overridden Entrypoint.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
The browser-view samples crawled crawlee.dev; from CI runners that
navigation intermittently timed out on every engine, failing one of the
two browser-view tests in roughly every other run. The Actors now start
from the runtime's console at http://apify-api:3000/, reachable from every
Actor container on every engine through the same route as the API, with
enough interlinked pages for the counts the tests assert.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
The browser-view Actors must crawl a real external site: routing them to
the runtime's own console would let a network-setup mismatch on the
private route go unnoticed.

This reverts commit 46cc7b56ab2bbc0f1b78ea3b5c4e11f1a0e1c8be.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
Their Actors crawl a real external site on purpose, and CI runners
occasionally see that site's navigations time out; a single retry keeps a
slow spell from failing the job while a reproducible defect still does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
The retry-on-default-network path in startRun and the sidecar launch
wrapper in startBrowserViewer were an intermediate attempt from before
Podman 3.x detection landed in init(). Since Podman 3.x now decides at
startup that Actors run on the engine's default network, the runtime
fallback never fires; drop it with its tests, and tidy the driver header
and docs to describe only the final behaviour.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
…n run

The retry added to the browser-view cases could never pass: the retried
attempt pushed the Actor again and the CLI refused ("already exists ...
newer changes than your local copy"). The suite now pushes in beforeAll
and both cases, retried or not, run the Actor that push produced.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CZhuUP13NByepkP4n3sg3P
@Pijukatel
Pijukatel merged commit d2f88ce into master Sep 10, 2026
17 checks passed
@Pijukatel
Pijukatel deleted the claude/quirky-carson-9147px branch September 10, 2026 12:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants